package pl.cezary.butler.walili.common;

import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;
import pl.cezary.butler.walili.client.config.ClientConfiguration;
import pl.cezary.butler.walili.client.utils.DomainComparator;
import pl.cezary.butler.walili.server.shuffling.IShufflable;

public class Task implements Serializable, Cloneable, Comparable, IShufflable {

    static final long serialVersionUID = 3059146523347998236L;
    private static final Logger log = Logger.getLogger(Task.class.getName());
    private static final Set<String> PROTOCOLS;
    private static final Pattern urlPattern = Pattern.compile("\\w+:[\\/]{2}[a-z0-9]+(\\.[a-z0-9?=&;]+[\\/]?)*");
    private static final String FALLBACK_TYPE = "text/html";
    private static final int DIFFERENT_SERVER_HANDICAP = ClientConfiguration.getInstance().getDifferentServerPriorityHandicap();
    private static final MimeTypeGuesser MIME_TYPE_GUESSER = new MimeTypeGuesser();
    private String mimeType;
    private transient Integer shuffleId;

    static {
        Set<String> p = new HashSet<String>(2);
        p.add("http");
        p.add("ftp");
        PROTOCOLS = Collections.unmodifiableSet(p);
    }
    protected int priorytet;
    /**
     * <p style="margin-top: 0"> Adres url na kt&#243;rym wyst&#281;puje link z
     * zadania </p> <p style="margin-top: 0"> <br> Je&#380;eli pole jest puste
     * oznacza to &#380;e zadanie zosta&#322;o zg&#322;oszone bezpo&#347;rednio
     * przez u&#380;ytkownika. </p>
     */
    private URL polozenie;
    /**
     * <p style="margin-top: 0"> Niepusty napis wskazujacy na miejsce gdzie
     * znajduje si&#281; zas&#243;b kt&#243;ry nale&#380;y zweryfikowa&#263;.
     * Nie mo&#380;e by&#263; pusty i musi by&#263; poprawnym adresem url. </p>
     */
    private String cel;
    private URL celURL;
    private TaskStatus status;
    private boolean czyPoprawny = true;

    public boolean isInitialized() {
        return cel != null;
    }

    protected Task() {
        this.status = TaskStatus.NOWE;
    }

    public Task(String cel) {
        this();
        setCel(cel);
    }

    protected Task(Task zad) {
        this.setMimeType(zad.getMimeType());
        this.setPolozenie(zad.polozenie);
        this.setCel(zad.cel);
        this.status = zad.status;
        this.priorytet = zad.priorytet;
    }

    protected Task(ZadResztaVO zadResztaVO, URL polozenie) {
        this.mimeType = zadResztaVO.getMimeType();
        this.setPolozenie(polozenie);
        this.setCel(zadResztaVO.getCel());
        this.status = zadResztaVO.getStatus();
        this.priorytet = zadResztaVO.getPriorytet();
    }

    @Override
    public int hashCode() {
        final URL tmp = getCelURL();
        return (tmp != null) ? tmp.hashCode() : 0;

    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Task) {
            final URL temp = getCelURL();
            return (temp != null) ? getCelURL().equals(((Task) obj).getCelURL()) : false;
        }
        return false;
    }

    /**
     * Metoda pozwalajaca na pobranie adresu docelowego zadania w postaci
     * napisu.
     *
     * @return adres docelowy
     */
    public String getCel() {
        return cel;
    }

    /**
     * Metoda pozwalajaca na pobranie adresu polozenia zadania w postaci napisu.
     *
     * @return polozenie linka
     */
    public String getPolozenie() {
        return (polozenie != null) ? polozenie.toString() : null;
    }

    public URL getPolozenieURL() {
        return polozenie;
    }

    /**
     * Metoda pozwalajaca na pobranie priorytetu zadania
     *
     * @return priorytet
     */
    public int getPriorytet() {
        return priorytet;
    }

    /**
     * <p style="margin-top: 0"> Zwraca url zawierajacy cel, wygenerowany na
     * podstawie polozenia adresu oraz celu. </p>
     */
    public URL getCelURL() {
        updateCelUrl();
        return celURL;
    }

    /**
     * <p style="margin-top: 0"> Metoda sprawdzajaca czy cel jest ignorowalny.
     * <br>Ignorowalne moglyby by&#263; adresy wyko&#380;ystuj&#261;ce
     * egzotyczne protoko&#322;y jak r&#243;wnie&#380; odno&#347;niki do
     * zak&#322;adek na tej samej stronie. </p>
     */
    public boolean ignorable() {
        if (cel.startsWith("#") || !isValidURL()) {
            return true;
        } else {
            final URL tmp = getCelURL();
            if (tmp == null || !PROTOCOLS.contains(tmp.getProtocol())) {
                return true;
            }
        }
        return false;
    }

    /**
     * <p style="margin-top: 0"> Weryfikuje czy cel mo&#380;e by&#263; poprawnym
     * adresem URL. </p>
     */
    public boolean isValidURL() {
        updateCelUrl();
        return czyPoprawny;
    }

    public Task tworzZadanie(String cel) {
        Task zad = (Task) this.clone();
        zad.status = TaskStatus.NOWE;
        zad.setPolozenie(this.getCelURL());
        zad.setCel(cel);
        zad.incrementPriorytet();

        return zad;
    }

    /**
     * Metoda pozwalajaca na pobranie aktualnego statusu zadania
     *
     * @return aktualny status
     */
    public TaskStatus getStatus() {
        return status;
    }

    /**
     * Metoda pozwalajaca na zmiane aktualnego statusu. Metoda zmienia status
     * tylko wtedy gdy jest zgodny z diagramem stanu statusu. W przeciwnym
     * przypadku nie modyfikuje statusu, i zapisuje informacje w logu.
     * <i>Tymczasowo mozliwe bedzie rowniez wyrzucenie wyjatku czasu
     * wykonania</i>
     *
     * @param val
     */
    public void setStatus(TaskStatus val) {
        if (this.status.czyPrzejscieDozwolone(val)) {
            this.status = val;
        } else {
            log.log(Level.SEVERE, "Próba dokonania niedozwolonego przejścia między stanami", new IllegalStatusChange(this.status, val));
        }
    }

    protected final void setCel(String val) {
        if (val == null || !val.equals(this.cel)) {
            celURL = null;
            this.cel = val;
            updateCelUrl();
            if (mimeType == null) {//FIXME te sety to chyba nie był taki dobry pomysł                
                setMimeType(MIME_TYPE_GUESSER.guessMimeType(getCelURL()));
            }
        }
    }

    protected final void setPolozenie(String val) {
        try {
            if (val != null) {
                setPolozenie(new URL(val));
            } else {
                this.polozenie = null;
            }
        } catch (MalformedURLException ex) {
            this.polozenie = null;
            czyPoprawny = false;
            log.log(Level.SEVERE, String.format("Wprowadzono niepoprawny adres URL: %s", celURL), ex);
        }
    }

    private void setCel(URL cel) {
        if (cel != null && !cel.equals(this.celURL)) {
            this.cel = cel.toString();
            this.celURL = cel;
            setMimeType(MIME_TYPE_GUESSER.guessMimeType(getCelURL()));
        }
    }

    private void updateCelUrl() {
        try {
            if (celURL != null) {
            } else if (cel != null && cel.length() > 0) {
                this.celURL = new URL(polozenie, cel);
                czyPoprawny = true;
            } else {
                czyPoprawny = false;
            }
        } catch (MalformedURLException e) {
            czyPoprawny = false;
            log.log(Level.SEVERE, "Illegal URL: {0} found on {1}", new Object[]{celURL == null ? cel : celURL, polozenie});
        }
    }

    protected final void setPolozenie(URL polozenie) {
        this.polozenie = polozenie;
        this.celURL = null;
    }
    private static final Comparator<String> DOMAIN_COMPARATOR = DomainComparator.getInstance();

    /**
     * <p style="margin-top: 0"> Powoduje obni&#380;enie priorytetu
     * (r&#243;wnoznaczne z inkrementacj&#261; zmiennej priorytet) </p>
     */
    protected void incrementPriorytet() {
        //TODO dobre miejsce na wykorzystanie strategii
        if (polozenie != null && getCelURL() != null) {
            int dif;
            if ((dif = Math.abs(
                    DOMAIN_COMPARATOR.compare(polozenie.getHost(), celURL.getHost()))) > 11.) {
                priorytet += Math.ceil(Math.log(dif) * DIFFERENT_SERVER_HANDICAP);
                return;
            }
        }
        ++priorytet;
    }

    @Override
    public Task clone() {
        return new Task(this);
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof Task) {
            Task zad = (Task) o;
            return cel.compareTo(zad.cel);
        } else {
            return Integer.MIN_VALUE;
        }
    }

    protected void initStatus(TaskStatus status) {
        this.status = status;
    }

    public void updatePriorytet(Task zad) {
        if (equals(zad)) {
            priorytet = Math.min(priorytet, zad.getPriorytet());
        }
    }

    public static Task trim(Task z) {
        Task rt = new Task();
        rt.cel = z.cel;
        rt.celURL = z.celURL;
        rt.czyPoprawny = z.czyPoprawny;
        rt.polozenie = z.polozenie;
        rt.priorytet = z.priorytet;
        rt.status = z.status;
        rt.mimeType = z.mimeType;
        return rt;
    }

    public static List<Task> trim(List<? extends Task> zadania) {
        final List<Task> rt = new ArrayList<Task>(zadania.size());
        final Iterator<? extends Task> i = zadania.iterator();
        while (i.hasNext()) {
            rt.add(trim(i.next()));
        }
        return rt;
    }

    @Override
    public String toString() {
        return "Task{" + "priorytet=" + priorytet + ", polozenie=" + polozenie + ", cel=" + cel + ", celURL=" + celURL + ", status=" + status + ", czyPoprawny=" + czyPoprawny + ", shuffleId=" + shuffleId + '}';
    }

    public void setPriorytet(int priorytet) {
        this.priorytet = priorytet;
    }

    public String getMimeType() {
        //assert (mimeType == null || mimeType.matches("^\\w+/[-+a-zA-Z0-9]+$"));
        return mimeType;
    }

    public final void setMimeType(final String mime) {
        //assert (mime == null || mime.matches("^\\w+/[-+a-zA-Z0-9]+$")) : String.format("Typ %s jest niepoprawny", mime);
        this.mimeType = mime;
    }

    @Override
    public Integer getShuffleId() {
        return shuffleId;
    }

    @Override
    public void setShuffleId(Integer sid) {
        shuffleId = sid;
    }
}